/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.milton.http;

import java.io.IOException;
import java.io.OutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;

/**
 * Lightweight XML generation. Gives the programmer fine grained control of the
 * generated xml, including whitespace.
 * <P/>
 * The XML is not guaranteed to be parseable.
 *
 * @author brad
 */
public class XmlWriter {

	private Logger log = LoggerFactory.getLogger(XmlWriter.class);

	public static Charset utf8() {
		return Charset.forName("UTF-8");
	}

	public enum Type {

		OPENING,
		CLOSING,
		NO_CONTENT
	}

	private boolean allowNewlines = false;
	protected final OutputStream out;
	protected final Charset charset;

	public XmlWriter(OutputStream out, Charset charset) {
		this.out = out;
		this.charset = charset;
	}

	public XmlWriter(OutputStream out) {
		this.out = out;
		this.charset = XmlWriter.utf8();
	}

	/**
	 * Append the given raw String to the ouput. No encoding is applied
	 *
	 * @param value
	 */
	private void append(String value) {
		try {
			byte[] arr = value.getBytes(charset);
			out.write(arr);
		} catch (IOException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * Append the given character to the output. No encoding is applied
	 *
	 * @param c
	 */
	private void append(char c) {
		try {
			out.write(c);
		} catch (IOException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * Convenience method to write a single element containing a piece of text
	 *
	 *
	 * @param namespace - optional, namespace prefix
	 * @param namespaceInfo - optional, namespace url
	 * @param name - the local name of the element to create
	 * @param value - the raw text to insert into the element
	 */
	public void writeProperty(String namespace, String namespaceInfo, String name, String value) {
		writeElement(namespace, namespaceInfo, name, Type.OPENING);
		append(value);
		writeElement(namespace, namespaceInfo, name, Type.CLOSING);

	}

	public void writeProperty(String namespace, String name, String value) {
		if (value == null) {
			writeProperty(namespace, name);
		} else {
			writeElement(namespace, name, Type.OPENING);
			append(value);
			writeElement(namespace, name, Type.CLOSING);
		}
	}

	public void writeProperty(String namespace, String name) {
		writeElement(namespace, name, Type.NO_CONTENT);
	}

	public void writeProperty(String name) {
		writeElement(null, name, Type.NO_CONTENT);
	}

	public void writeElement(String namespace, String name, Type type) {
		writeElement(namespace, null, name, type);
	}

	/**
	 * Write an opening tag
	 *
	 * @param namespace
	 * @param name
	 */
	public void open(String namespace, String name) {
		writeElement(namespace, name, Type.OPENING);
	}

	/**
	 * Write a closing tag, Eg </name>
	 *
	 * @param namespace
	 * @param name
	 */
	public void close(String namespace, String name) {
		writeElement(namespace, name, Type.CLOSING);
	}

	/**
	 * Write an opening tag
	 *
	 * @param name
	 */
	public void open(String name) {
		writeElement(null, name, Type.OPENING);
	}

	/**
	 * Write a closing tag for the given name
	 *
	 * @param name
	 */
	public void close(String name) {
		writeElement(null, name, Type.CLOSING);
	}

	/**
	 * Represents an element which is currently being written
	 *
	 */
	public class Element {

		private final Element parent;
		private final String nsPrefix;
		private final String name;
		private boolean openEnded;

		/**
		 * Create the element and write the first part of the opening tag
		 *
		 * Eg <name
		 *
		 *
		 *
		 *
		 *

		 *
		 * @param name
		 */
		Element(Element parent, String name) {
			this(parent, null, name);
		}

		/**
		 * Create the element and write the first part of the opening tag
		 *
		 * Eg <name
		 *
		 *
		 *
		 *
		 *

		 *
		 * @param nsPrefix
		 * @param name
		 */
		Element(Element parent, String nsPrefix, String name) {
			this.parent = parent;
			this.name = name;
			this.nsPrefix = nsPrefix;
			append("<");
			if (nsPrefix != null) {
				append(nsPrefix);
				append(":");
			}
			append(name);
		}

		Element(Element parent, String uri, String nsPrefix, String name) {
			this.parent = parent;
			this.name = name;
			this.nsPrefix = nsPrefix;
			append("<");
			if (nsPrefix != null) {
				append(nsPrefix);
				append(":");
			}
			append(name);
			append(" ");
			append("xmlns:" + nsPrefix + "=\"");
			append(uri);
			append("\"");
		}

		/**
		 * Write a name/value attribute pair
		 *
		 * @param name
		 * @param value
		 * @return
		 */
		public Element writeAtt(String name, String value) {
			append(" ");
			append(name);
			append("=");
			append((char) 34);
			append(value == null ? "" : value);
			append((char) 34);
			return this;
		}

		/**
		 * Write the text into the element. Will finish the opening tag if
		 * required
		 *
		 * @param text
		 * @return
		 */
		public Element writeText(String text) {
			return writeText(text, true);
		}

		public Element writeText(String text, boolean newline) {
			if (!openEnded) {
				open(newline);
			}
			append(text);
			return this;
		}

		/**
		 * Completes the opening tag which is started in the constructor. And
		 * writes a new line
		 *
		 * Eg >
		 *
		 * @return
		 */
		public Element open() {
			return open(true);
		}

		public Element open(boolean newline) {
			openEnded = true;
			append(">");
			if (newline) {
				newLine();
			}
			return this;
		}

		/**
		 * Closes the tag by determining its current state. Can close with a
		 * no-content tag </name> if no content has been written, or with write
		 * a close tag
		 *
		 * @return - the parent element
		 */
		public Element close() {
			return close(false);
		}

		public Element close(boolean newline) {
			if (openEnded) {
				if (nsPrefix != null) {
					append("</" + nsPrefix + ":" + name + ">");
					newLine();
				} else {
					append("</" + name + ">");
					newLine();
				}
				if (newline) {
					newLine();
				}
				return parent;
			} else {
				if (newline) {
					newLine();
				}
				return noContent();
			}
		}

		/**
		 * Write a self closing tag, eg />
		 *
		 * @return - the parent element
		 */
		public Element noContent() {
			append("/>");
			newLine();
			return parent;
		}

		public Element noContent(boolean newLine) {
			append("/>");
			if (newLine) {
				newLine();
			}
			return parent;
		}

		/**
		 * Start a new element, completing the open tag if required
		 *
		 * @param name
		 * @return
		 */
		public Element begin(String name) {
			return begin(null, name);
		}

		public Element begin(String prefix, String name) {
			return begin(prefix, name, true);
		}

		public Element begin(String prefix, String name, boolean newLine) {
			if (!openEnded) {
				open(newLine);
			}

			return new Element(this, prefix, name);
		}

		public Element begin(String uri, String prefix, String name) {
			if (!openEnded) {
				open();
			}

			return new Element(this, uri, prefix, name);
		}

		/**
		 * Write a property element like - <name>value</name>
		 *
		 * @param name
		 * @param value
		 * @return
		 */
		public Element prop(String name, String value) {
			begin(name).writeText(value, false).close(true);
			return this;
		}

		public Element prop(String name, Integer value) {
			if (value != null) {
				prop(name, value.toString());
			} else {
				begin(name).noContent();
			}
			return this;
		}
	}

	public Element begin(String name) {
		return new Element(null, name);
	}

	public Element begin(String nsPrefix, String name) {
		return new Element(null, nsPrefix, name);
	}

	public void writeElement(String nsPrefix, String nsUrl, String name, Type type) {
		if ((nsPrefix != null) && (nsPrefix.length() > 0)) {
			switch (type) {
				case OPENING:
					if (nsUrl != null) {
						append("<" + nsPrefix + ":" + name + " xmlns:" + nsPrefix + "=\"" + nsUrl + "\">");
					} else {
						append("<" + nsPrefix + ":" + name + ">");
					}
					break;
				case CLOSING:
					append("</" + nsPrefix + ":" + name + ">");
					newLine();
					break;
				case NO_CONTENT:
				default:
					if (nsUrl != null) {
						append("<" + nsPrefix + ":" + name + " xmlns:" + nsPrefix + "=\"" + nsUrl + "\"/>");
					} else {
						append("<" + nsPrefix + ":" + name + "/>");
					}
					break;
			}
		} else {
			switch (type) {
				case OPENING:
					append("<" + name + ">");
					break;
				case CLOSING:
					append("</" + name + ">\n");
					break;
				case NO_CONTENT:
				default:
					append("<" + name + "/>");
					break;
			}
		}
	}

	/**
	 * Append plain text.
	 *
	 * @param text Text to append
	 */
	public void writeText(String text) {
		append(text);
	}

	/**
	 * Write a CDATA segment.
	 *
	 * @param data Data to append
	 */
	public void writeData(String data) {
		append("<![CDATA[" + data + "]]>");
	}

	public void writeXMLHeader() {
		append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n");
	}

	/**
	 * Send data and reinitializes buffer.
	 */
	public void flush() {
		try {
			out.flush();
		} catch (IOException ex) {
			throw new RuntimeException(ex);
		}
	}

	public void newLine() {
		if (allowNewlines) {
			append("\n");
		}
	}

	public boolean isAllowNewlines() {
		return allowNewlines;
	}

	public void setAllowNewlines(boolean allowNewlines) {
		this.allowNewlines = allowNewlines;
	}
}
